/*
	Copyright (c) 2007, ANYwebcam.com Pty Ltd. All rights reserved.

	The software in this package is published under the terms of the BSD style 
	license, a copy of which has been included with this distribution in the 
	license.txt file.
*/
package com.anywebcam.di
{
	import com.anywebcam.di.component.*;
	import com.anywebcam.di.error.*;
	import com.anywebcam.di.util.CyclicDependencyGuard;
	
	import flash.utils.Dictionary;
	import flash.utils.describeType;
	import flash.utils.getDefinitionByName;
	import flash.utils.getQualifiedClassName;
	import flash.utils.getQualifiedSuperclassName;
	
	/**
	 * Default Implementation of DIContainer
	 * 
	 * @example 
	 * <listing version="3.0">
	 * package
	 * {
	 * 	import com.anywebcam.di.*;
	 * 
	 * 	import mx.core.Application; 
	 * 
	 * 	public class MyApp extends Application
	 * 	{
	 * 		public var container:DIContainer;
	 * 
	 * 		public function MyApp()
	 * 		{
	 * 			super();
	 * 			container = new DefaultDIContainer();
	 * 
	 * 			// register your components
	 * 			container.register( DIContainer, container );
	 * 			container.register( Stage, stage );
	 * 			container.register( FrontController, MyFrontController );
	 * 			container.register( ModelLocator, MyAppModelLocator );
	 * 		}
	 * 	}
	 * }
	 * </listing>
	 */
	public class DefaultDIContainer implements DIContainer
	{
		/**
		 * Default options for all DefaultDIContainers. 
		 * <p>Used when container.options is not specified, or required options are not available on the container.options.</p>
		 * <p>Defaults are: instance cache enabled, and use setter injection. </p>
		 * <p>Changes will affect NEW DefaultDIContainers only, except when changing the options or injectionTypeToDIComponentMap instance properties.</p>
		 * 
		 * @example <listing version="3.0">DefaultDIContainer.defaultOptions = { cache: false, injectionType: 'constructor'}; </listing>
		 */
		public static var defaultOptions:Object = { cache: true, injectionType: 'setter' };

		/**
		 * Default map of injectionType to DIComponent implementations. 
		 * <p>Additional components can be registered to handle different cases if necessary.</p>
		 * <p>Defaults are: <ul><li>'setter': SetterInjectionComponent</li><li>'constructor': ConstructorInjectionComponent</li></ul></p>
		 * <p>Changes will affect NEW DefaultDIContainers only, except when changing the options or injectionTypeToDIComponentMap instance properties.</p>
		 *
		 * @see com.anywebcam.di.component.SetterInjectionComponent
		 * @see com.anywebcam.di.component.ConstructorInjectionComponent
		 * @example <listing version="3.0">DefaultDIContainer.defaultInjectionTypeToDIComponentMap = { 'setter' : MyCustomSetterInjectionComponent }; </listing>
		 */
		public static var defaultInjectionTypeToDIComponentMap:Object = 
		{
			'setter': 		SetterInjectionComponent,
			'constructor': 	ConstructorInjectionComponent
		};

		// the DIComponents
		private var _components:Array;
		
		// used when resolving ( key is Class ) to DIComponent
		private var _keyToComponentCache:Dictionary;
		
		// options for this instance of DefaultDIContainer
		private var _options:Object;
		
		// protects against component requesting themselves, and / or cyclic-dependencies between components
		private var _cyclicDependencyGuard:CyclicDependencyGuard;
		
		// per instance map of injection types to the appropriate component
		private var _injectionTypeToDIComponentMap:Object;
		
		/**
		 * Constructor
		 * 
		 * @param options An Object literal specifying cache: true | false, and injectionType: 'constructor' | 'setter'
		 * @param injectionTypeToDIComponentMap An Object literal specifying DIComponent subclasses to use for each injectionType, overrides the defaultInjectionTypeToDIComponentMap
		 */
		public function DefaultDIContainer( options:Object=null, injectionTypeToDIComponentMap:Object=null )
		{
			_components = new Array();
			_keyToComponentCache = new Dictionary();
			_cyclicDependencyGuard = new CyclicDependencyGuard();

			this.options = options;
			this.injectionTypeToDIComponentMap = injectionTypeToDIComponentMap;
		}
		
		/**
		 * An Object literal specifying whether instance caching is enabled, and what injection type to use by default. 
		 *
		 * @example <listing version="3.0">container.options = { cache: false, injectionType: 'constructor'}; </listing>
		 */
		public function get options():Object
		{
			return _options;
		}
		
		/**
		 * @private
		 */		
		public function set options( options:Object ):void
		{
			_options = mergeObjects( defaultOptions, options ); 
		}
		
		/**
		 * Map of the injection type strings to the Class to instantiate for that injection type
		 * 
		 * @example <listing version="3.0">container.injectionTypeToDIComponentMap = { 'setter' : MyCustomSetterInjectionComponent }; </listing>
		 */
		public function get injectionTypeToDIComponentMap():Object
		{
			return _injectionTypeToDIComponentMap;
		}
		
		/**
		 * @private
		 */
		public function set injectionTypeToDIComponentMap( value:Object ):void
		{
			_injectionTypeToDIComponentMap = mergeObjects( defaultInjectionTypeToDIComponentMap, value );
		}
		
		/**
		 * @inheritDoc
		 */
		public function register( ...args ):DIComponent
		{
			var component:DIComponent;
			
			// 0 args
			// throw errors
			if( args.length == 0 )
			{
				// fixme: should we wrap this error in DIError? 
				throw new ArgumentError('Insufficient arguments expects 1 or more');
			}
			
			// 1 arg
			// if is Class then is implementation
			// else is instance
			if( args.length == 1 )
			{
				if( args[0] is DIComponent )
				{
					component = registerComponent( args[0] as DIComponent );
				}
				else if( args[0] is Class )
				{
					component = registerType( args[0] as Class, args[0] as Class );
				}
				else
				{
					component = registerInstance( ((args[0]).constructor as Class), args[0] );
				}
			}
			
			// 2 args
			// key, implementation
			// key, instance
			if( args.length == 2 )
			{
				if( args[1] is Class )
				{
					component = registerType( args[0], args[1] as Class );
				}
				else
				{
					component = registerInstance( args[0], args[1] );
				}
			}
			
			// 3 args
			// key, implementation, parameters, componentOptions
			// key, instance, parameters			
			if( args.length >= 3 )
			{
				if( args[1] is Class )
				{
					component = registerType.apply( this, args );
				}
				else
				{
					component = registerInstance.apply( this, args );
				}
			}
			
			return component;
		}
		
		/**
		 * @inheritDoc
		 */
		public function registerComponent( component:DIComponent, ...args ):DIComponent
		{
			var key:Object = component.key;

			if( hasComponent( key ) ) throw new DIError( 'Key exists already: '+ key, new ArgumentError('Key exists already: '+ key) );
			
			_components.push( component );
			_keyToComponentCache[ key ] = component;
			return component;
		}
		
		/**
		 * @inheritDoc
		 */
		public function registerInstance( key:Object, instance:Object, ...args ):DIComponent
		{
			var parameters:Array = args ? args[0] as Array : null;
			var options:Object = args && args.length > 1 ? args[1] : null;
		
			return registerComponent( new InstanceComponent( key, instance, parameters, options ) );
		}
		
		/**
		 * @inheritDoc
		 */
		public function registerType( key:Object, type:Class, ...args ):DIComponent
		{
			var parameters:Array = args[0] as Array;
			var componentOptions:Object = mergeObjects( options, args[1] as Object ) || options;
			var impl:Class = (type == null && key is Class) ? type as Class : type as Class;
			
			if( !injectionTypeToDIComponentMap[ componentOptions.injectionType ] )
				throw new DIContainerError( this, 'Unknown injectionType:' + componentOptions.injectionType + ', for key:' + key + ', with impl:' + impl );
			
			var component:DIComponent = new (injectionTypeToDIComponentMap[ componentOptions.injectionType ] as Class)( key, impl, parameters );

			if( !component )
				throw new DIContainerError( this, 'Unable to instantiate component for key:' + key + ' with type of:' + impl );
			
			if( componentOptions.cache ) component = new CachingComponent( component );
			return registerComponent( component );
		}
		
		/**
		 * @inheritDoc
		 */
		public function getInstance( key:Object ):Object 
		{
			var instance:Object;
			var error:Error;
			
			// check for cyclic dependencies and check against them
			_cyclicDependencyGuard.guard( key );
			
			// try by key
			try
			{
				instance = getInstanceByKey( key );
			}
			catch( e:DIError )
			{
				error = e;
			}
			
			// try by type
			try
			{
				if( !instance && key is Class )
				{
					instance = getInstanceByType( key as Class );
				}
			}
			catch( e:DIError )
			{
				error = e;
			}
			
			// remove key from guard as instance should have been resolved 
			// we do this here to prevent errors from the keys being left in the guard in cases where the instance is null
			_cyclicDependencyGuard.unguard( key );
			
			if( instance == null ) throw (error ? error : new ArgumentError('Unknown key or type'));
			
			return instance;
		}
		
		/**
		 * @inheritDoc
		 */
		public function getInstanceByKey( key:Object ):Object 
		{
			if( !_keyToComponentCache[ key ] )
			{
				throw new DIError( 'Key not found: '+ key, new ArgumentError('Key not found: '+ key) );
			}
			
			return (_keyToComponentCache[ key ] as DIComponent).getInstance( this );
		}
		
		/**
		 * @inheritDoc
		 */
		public function getInstanceByType( type:Class ):Object
		{
			if( type == null )
			{
				throw new DIError( 'Type is null.', new ArgumentError('Type is null.') );
			}
			
			// loop over components
			// fixme: do this in reverse so that we check the most recently added components first
			for( var i:uint=0, n:uint=_components.length; i < n; i++ )
			{
				var component:DIComponent = _components[ i ] as DIComponent;

				// check if implementation satisfies type
				if( component.classInfo.isImplementorOf( type ) )
				{
					// todo: add component.key to _keyToComponentCache so we dont iterate as much on getInstance()
					// note about above todo:, it hasn't been necessary so far

					// request instance from the component
					return component.getInstance( this );
				}
			}
			
			throw new DIError( 'Type not found: '+ type + '. Stack:'+ _cyclicDependencyGuard.stack.join(', ') , new ArgumentError('Type not found: '+ type) );
			
			return null;
		}
	
		/**
		 * @inheritDoc
		 */
		public function getComponent( key:Object ):DIComponent 
		{
			if( _keyToComponentCache[ key ] != null )
			{
				return _keyToComponentCache[ key ] as DIComponent;
			}
			return null;
		}
	
		/**
		 * @inheritDoc
		 */
		public function unregisterComponent( component:DIComponent ):DIComponent
		{
			var existingComponentIndex:int = components.indexOf( component );
			
			if( existingComponentIndex > -1 )
			{
				components.splice( existingComponentIndex, 1 );
			}
			
			if( _keyToComponentCache[ component.key ] != null )
			{
				_keyToComponentCache[ component.key ] = null;
				delete _keyToComponentCache[ component.key ];
			}
			
			return component;			
		}
	
		/**
		 * @inheritDoc
		 */
		public function get components():Array
		{
			return _components;
		}
		
		/**
		 * @inheritDoc
		 */
		public function hasComponent( key:Object ):Boolean
		{
			// todo: if key is class iterate over components and check if component.classInfo.isImplementorOf( key as Class );
			
			return ( _keyToComponentCache[ key ] != null ) ? true : false;
		}
		
		/**
		 * Merges two simple Objects together into a new Object, values from a are overwritten by values from b
		 * 
		 * @param a A simple Object
		 * @param b A simple Object
		 * @return A new simple Object with the properties and values from a & b
		 */
		protected function mergeObjects( a:Object, b:Object ):Object
		{
			var c:Object = {};
			
			if( a ) for( var aKey:String in a ) c[ aKey ] = a[ aKey ];
			if( b ) for( var bKey:String in b ) c[ bKey ] = b[ bKey ];
			
			return c;
		}
		
		public function toString():String 
		{
			return '[DefaultDIContainer]';
		}
	}
}